using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Threading;
using System.Data.SQLite;
using System.Diagnostics;
using System.Globalization;
using LeisureCenter.Core;
using LeisureCenter.Tools;
using LeisureCenter.LeisureCenterDataSetTableAdapters;
using LeisureCenter.MetadataHandlers;

namespace LeisureCenter.LibraryBuilderThreads
{
    public class FilesCheckerThreadError : Exception
    {
        public FilesCheckerThreadError(string message, Exception innerException)
            : base(message, innerException)
        {
            
        }
    }

    public class FileCheckerFile
    {
        public FileInfo File;
        public bool ForceRefresh;

        public FileCheckerFile(FileInfo file, bool forceRefresh)
        {
            this.File = file;
            this.ForceRefresh = forceRefresh;
        }
    }

    public class FilesCheckerThread : SleepingThread
    {
        private Queue<FileCheckerFile> files = new Queue<FileCheckerFile>();
        private bool paused = true;
        private Object filesLock = new Object();
        private int filesCount;
        private int checkedFilesCount;
        private int newFilesCount;
        private int changedFilesCount;
        private int knownFilesCount;
        private MetadataHandlersManager metadataHandlersManager = new MetadataHandlersManager();
        private FilesTableAdapter filesTableAdapter = new FilesTableAdapter();
        private List<long> checkedFilesIds = new List<long>();
        private long startDatetime;
        private Dictionary<string, bool> checkedDrives = new Dictionary<string, bool>(StringComparer.InvariantCultureIgnoreCase);

        private static long GetLibraryId(string fileName)
        {
            IEnumerable<KeyValuePair<string, long>> paths = DatabaseManager.ExecuteStringsLongsQuery(@"SELECT name, libraryId FROM Paths ORDER BY length(name) DESC");
            foreach (KeyValuePair<string, long> path in paths)
                if (fileName.StartsWith(path.Key))
                    return path.Value;
            return 1;
        }
        
        private void SetAudioFileMetaData(AudioMetadataHandler metadataHandler, SQLiteTransaction transaction, string id)
        {
            using (SQLiteCommand command = filesTableAdapter.Connection.CreateCommand())
            {
                command.Transaction = transaction;
                command.CommandText = String.Format(@"insert or replace into AudioFilesMetaData (id, artist, album, track, title, year, genre) values ({6}, '{0}', '{1}', {2}, '{3}', {4}, '{5}')",
                    SQLiteTools.EscapeSqlString(metadataHandler.Artist), SQLiteTools.EscapeSqlString(metadataHandler.Album), SQLiteTools.ReturnNullIfNeeded(metadataHandler.Track), SQLiteTools.EscapeSqlString(metadataHandler.Title), SQLiteTools.ReturnNullIfNeeded(metadataHandler.Year), SQLiteTools.EscapeSqlString(metadataHandler.Genre), id);
                command.ExecuteNonQuery();
            }
            return;
        }

        private void SetVideoFileMetaData(VideoMetadataHandler metadataHandler, SQLiteTransaction transaction, string id)
        {
            using (SQLiteCommand command = filesTableAdapter.Connection.CreateCommand())
            {
                command.Transaction = transaction;
                command.CommandText = String.Format(@"INSERT OR REPLACE INTO VideoFilesMetaData (Id, Title, Subtitle, Number, SubNumber, Type) VALUES ({0}, '{1}', '{2}', {3}, {4}, {5})",
                    id, SQLiteTools.EscapeSqlString(metadataHandler.Title), SQLiteTools.EscapeSqlString(metadataHandler.Subtitle), SQLiteTools.ReturnNullIfNeeded(metadataHandler.Number), SQLiteTools.ReturnNullIfNeeded(metadataHandler.SubNumber), SQLiteTools.ReturnNullIfNeeded(metadataHandler.Type));
                command.ExecuteNonQuery();
            }
            return;
        }

        private void SetFileMetaData(MetadataHandler metadataHandler, SQLiteTransaction transaction, string id)
        {
            if (metadataHandler is AudioMetadataHandler)
            {
                SetAudioFileMetaData((AudioMetadataHandler)metadataHandler, transaction, id);
            }
            else
                if (metadataHandler is VideoDefaultMetadataHandler)
                {
                    SetVideoFileMetaData((VideoDefaultMetadataHandler)metadataHandler, transaction, id);
                }
        }

        private void AddOrUpdateFile(MetadataHandler metadataHandler, string id, string query)
        {
            
            filesTableAdapter.Connection.Open();
            try
            {
                using (SQLiteTransaction transaction = filesTableAdapter.Connection.BeginTransaction())
                {
                    try
                    {
                        using (SQLiteCommand command = filesTableAdapter.Connection.CreateCommand())
                        {
                            command.Transaction = transaction;
                            command.CommandText = query;
                            command.ExecuteNonQuery();
                        }

                        SetFileMetaData(metadataHandler, transaction, id);
                        transaction.Commit();
                    }
                    catch
                    {
                        transaction.Rollback();
                        throw;
                    }
                }
            }
            finally
            {
                filesTableAdapter.Connection.Close();
            }
        }

        public void AddFileToTable(FileInfo file)
        {
            Trace.WriteLine(string.Format("Reading file '{0}' metadata...", file.FullName));
            int start = Environment.TickCount;
            try
            {
                MetadataHandlers.MetadataHandlerFactory metadataHandlerFactory = metadataHandlersManager.GetAppropriateMetaDataHandler(file.FullName);
                if (metadataHandlerFactory != null)
                {
                    MetadataHandlers.MetadataHandler metadataHandler = metadataHandlerFactory.CreateMetadata();
                    try
                    {
                        metadataHandler.ReadMetadata(file.FullName, true);
                    }
                    catch (Exception ex)
                    {
                        throw new Exception(String.Format(@"An error occured while reading '{0}' metadata", file.FullName), ex);
                    }

                    LeisureCenterDataSet.FilesDataTable filesDataTable;

                    filesDataTable = filesTableAdapter.GetFilesByFilename(file.FullName);
                    if (filesDataTable != null && filesDataTable.Count > 0)
                    {
                        // this file is already in the library the same filename but hasn't been hashed
                        LeisureCenterDataSet.FilesRow row = (LeisureCenterDataSet.FilesRow)filesDataTable.Rows[0];
                        if (row.IsAddedDatetimeNull())
                            row.AddedDatetime = DateTime.Now.ToFileTime();

                        string query = string.Format(@"update Files set lastModified = '{0}', updatedDatetime = {2}, dataHash = '{3}', kind = 1, addedDatetime = {4}, libraryId = {5} where id = {1}",
                                    file.LastWriteTime.ToFileTime(), row.Id, DateTime.Now.ToFileTime(), metadataHandler.DataHash, row.AddedDatetime, GetLibraryId(file.FullName));
                        AddOrUpdateFile(metadataHandler, row.Id.ToString(CultureInfo.InvariantCulture), query);
                        return;
                    }

                    filesDataTable = filesTableAdapter.GetFilesByDataHash(metadataHandler.DataHash);
                    if (filesDataTable != null && filesDataTable.Count > 0)
                    {
                        // this file is already in the library with another filename
                        foreach (LeisureCenterDataSet.FilesRow row in filesDataTable)
                        {
                            if (!File.Exists(row.Filename))
                            {
                                // the file stored in the library has been deleted, we assume is has been moved to this new location
                                string query = string.Format(@"update Files set filename = '{0}', lastModified = '{1}', updatedDatetime = {3}, libraryId = {4} where id = {2}",
                                            SQLiteTools.EscapeSqlString(file.FullName), file.LastWriteTime.ToFileTime(), row.Id, DateTime.Now.ToFileTime(), GetLibraryId(file.FullName));
                                AddOrUpdateFile(metadataHandler, row.Id.ToString(CultureInfo.InvariantCulture), query);
                                return;
                            }
                        }
                    }

                    // this file doesn't exist in the library, we add it
                    {
                        string query = string.Format(@"insert or replace into Files (filename, dataHash, kind, lastModified, addedDatetime, updatedDatetime, libraryId) values ('{0}', '{1}', {2}, {3}, {4}, {4}, {5})",
                                    SQLiteTools.EscapeSqlString(file.FullName), metadataHandler.DataHash, 1, file.LastWriteTime.ToFileTime(), DateTime.Now.ToFileTime(), GetLibraryId(file.FullName));
                        AddOrUpdateFile(metadataHandler, @"last_insert_rowid()", query);
                        return;
                    }
                }
            }
            catch (FileNotFoundException ex)
            {
                Trace.WriteLine(ex.Message);
            }
            finally
            {
                Trace.WriteLine(string.Format("Finished in {0}ms", Environment.TickCount - start));
            }
        }

        private void CheckFile(FileCheckerFile file)
        {
            try
            {
                try
                {
                    if (file.ForceRefresh)
                    {
                        AddFileToTable(file.File);
                        changedFilesCount++;
                    }
                    else
                    {
                        LeisureCenterDataSet.FilesDataTable filesDataTable = filesTableAdapter.GetFilesByFilename(file.File.FullName);
                        if (filesDataTable != null && filesDataTable.Count > 0)
                        {
                            // this file has already been checked once
                            LeisureCenterDataSet.FilesRow row = filesDataTable[0];
                            if (row.IsLastModifiedNull() || row.LastModified != file.File.LastWriteTime.ToFileTime())
                            {
                                // the file has changed since last check
                                AddFileToTable(file.File);
                                changedFilesCount++;
                            }
                            else
                            {
                                knownFilesCount++;
                                this.checkedFilesIds.Add(row.Id);
                            }
                        }
                        else
                        {
                            // this is the first time we check this file
                            AddFileToTable(file.File);
                            newFilesCount++;
                        }
                    }
                }
                catch (PathTooLongException ex)
                {
                    throw new FilesCheckerThreadError(string.Format(@"An error occured checking file '{0}'", file.File.Name), ex);
                }
                catch (Exception ex)
                {
                    throw new FilesCheckerThreadError(string.Format(@"An error occured checking file '{0}'", file.File.FullName), ex);
                }
            }
            catch(Exception ex)
            {
                Program.LeisureCenterInstance.HandleException(ex);
            }
            finally
            {
                checkedFilesCount++;
            }
        }

        protected override void DoWork()
        {
            this.startDatetime = DateTime.Now.ToFileTime();
            this.checkedDrives.Clear();

            // check for new files
            while (!terminated && !paused)
            {
                FileCheckerFile file;
                lock (this.filesLock)
                {
                    if (files.Count > 0)
                        file = files.Dequeue();
                    else
                        break;
                }

                string fullName;

                try
                {
                    fullName = file.File.FullName;
                }
                catch (ArgumentException)
                {
                    continue;
                }

                if (DriveIsAccessible(fullName))
                    CheckFile(file);
            }

            // check for deleted files
            if (!terminated && !paused)
            {

                LeisureCenterDataSet.FilesDataTable uncheckedFiles = filesTableAdapter.GetUndeletedFilesBeforeUpdateDatetime(this.startDatetime);
                this.filesCount = uncheckedFiles.Count;
                this.checkedFilesCount = 0;
                this.filesTableAdapter.Connection.Open();
                try
                {
                    SQLiteTransaction transaction = this.filesTableAdapter.Connection.BeginTransaction();
                    try
                    {
                        foreach (LeisureCenterDataSet.FilesRow uncheckedFile in uncheckedFiles)
                        {
                            try
                            {
                                if (terminated || paused)
                                    break;

                                if (this.checkedFilesIds.Contains(uncheckedFile.Id))
                                    continue;

                                if (!DriveIsAccessible(uncheckedFile.Filename))
                                    continue;

                                int deletedValue = 0;
                                if (!File.Exists(uncheckedFile.Filename))
                                    deletedValue = 1;

                                if (deletedValue == 1 || uncheckedFile.IsDeletedNull())
                                {
                                    SQLiteCommand command = this.filesTableAdapter.Connection.CreateCommand();
                                    command.Transaction = transaction;
                                    command.CommandText = String.Format(@"update Files set deleted = {1}, updatedDatetime = {2} where id = {0};", uncheckedFile.Id, deletedValue, DateTime.Now.ToFileTime());
                                    command.ExecuteNonQuery();
                                }
                            }
                            finally
                            {
                                this.checkedFilesCount++;
                            }
                        }
                        transaction.Commit();
                    }
                    catch
                    {
                        transaction.Rollback();
                    }
                }
                finally
                {
                    this.filesTableAdapter.Connection.Close();
                }
            }

            // check for unhashed files
            if (!terminated && !paused)
            {
                LeisureCenterDataSet.FilesDataTable unHashedFiles = filesTableAdapter.GetUnHashedFiles();
                this.filesCount = unHashedFiles.Count;
                this.checkedFilesCount = 0;
                foreach (LeisureCenterDataSet.FilesRow unHashedFile in unHashedFiles)
                {
                    try
                    {
                        if (!DriveIsAccessible(unHashedFile.Filename))
                            continue;

                        AddFileToTable(new FileInfo(unHashedFile.Filename));
                    }
                    finally
                    {
                        this.checkedFilesCount++;
                    }
                }
            }
        }

        private bool DriveIsAccessible(string path)
        {
            string drive = Path.GetPathRoot(path);

            if (!checkedDrives.ContainsKey(drive))
            {
                try
                {
                    string[] entries = Directory.GetFileSystemEntries(drive);
                    if (entries != null && entries.Length > 0)
                        checkedDrives.Add(drive, true);
                }
                catch (Exception)
                {
                    checkedDrives.Add(drive, false);
                }
            }
            return checkedDrives[drive];
        }

        public void AddFile(FileInfo file, bool forceRefresh)
        {
            lock(filesLock)
            {
                files.Enqueue(new FileCheckerFile(file, forceRefresh));
                filesCount++;
            }
        }

        public void AddFile(FileInfo file)
        {
            AddFile(file, false);
        }

        public void Pause()
        {
            paused = true;
        }

        public override void Wakeup()
        {
            paused = false;
            base.Wakeup();
        }

        public bool Paused { get { return paused; } }

        public int FilesCount { get { return filesCount; } }
        public int CheckedFilesCount { get { return checkedFilesCount; } }
        public int NewFilesCount { get { return newFilesCount; } }
        public int ChangedFilesCount { get { return changedFilesCount; } }
        public int KnownFilesCount { get { return knownFilesCount; } }
    }
}
